using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Spectre.Console
{
    /// <summary>
    /// Represents a prompt.
    /// </summary>
    /// <typeparam name="T">The prompt result type.</typeparam>
    public sealed class TextPrompt<T> : IPrompt<T>
    {
        private readonly string _prompt;

        /// <summary>
        /// Gets or sets the prompt style.
        /// </summary>
        public Style? PromptStyle { get; set; }

        /// <summary>
        /// Gets the list of choices.
        /// </summary>
        public HashSet<T> Choices { get; }

        /// <summary>
        /// Gets or sets the message for invalid choices.
        /// </summary>
        public string InvalidChoiceMessage { get; set; } = "[red]Please select one of the available options[/]";

        /// <summary>
        /// Gets or sets a value indicating whether input should
        /// be hidden in the console.
        /// </summary>
        public bool IsSecret { get; set; }

        /// <summary>
        /// Gets or sets the validation error message.
        /// </summary>
        public string ValidationErrorMessage { get; set; } = "[red]Invalid input[/]";

        /// <summary>
        /// Gets or sets a value indicating whether or not
        /// choices should be shown.
        /// </summary>
        public bool ShowChoices { get; set; } = true;

        /// <summary>
        /// Gets or sets a value indicating whether or not
        /// default values should be shown.
        /// </summary>
        public bool ShowDefaultValue { get; set; } = true;

        /// <summary>
        /// Gets or sets a value indicating whether or not an empty result is valid.
        /// </summary>
        public bool AllowEmpty { get; set; }

        /// <summary>
        /// Gets or sets the validator.
        /// </summary>
        public Func<T, ValidationResult>? Validator { get; set; }

        /// <summary>
        /// Gets or sets the default value.
        /// </summary>
        internal DefaultValueContainer? DefaultValue { get; set; }

        /// <summary>
        /// A nullable container for a default value.
        /// </summary>
        internal sealed class DefaultValueContainer
        {
            /// <summary>
            /// Gets the default value.
            /// </summary>
            public T Value { get; }

            /// <summary>
            /// Initializes a new instance of the <see cref="DefaultValueContainer"/> class.
            /// </summary>
            /// <param name="value">The default value.</param>
            public DefaultValueContainer(T value)
            {
                Value = value;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="TextPrompt{T}"/> class.
        /// </summary>
        /// <param name="prompt">The prompt markup text.</param>
        /// <param name="comparer">The comparer used for choices.</param>
        public TextPrompt(string prompt, IEqualityComparer<T>? comparer = null)
        {
            _prompt = prompt;

            Choices = new HashSet<T>(comparer ?? EqualityComparer<T>.Default);
        }

        /// <summary>
        /// Shows the prompt and requests input from the user.
        /// </summary>
        /// <param name="console">The console to show the prompt in.</param>
        /// <returns>The user input converted to the expected type.</returns>
        /// <inheritdoc/>
        public T Show(IAnsiConsole console)
        {
            if (console is null)
            {
                throw new ArgumentNullException(nameof(console));
            }

            var promptStyle = PromptStyle ?? Style.Plain;

            WritePrompt(console);

            while (true)
            {
                var input = console.ReadLine(promptStyle, IsSecret);

                // Nothing entered?
                if (string.IsNullOrWhiteSpace(input))
                {
                    if (DefaultValue != null)
                    {
                        console.Write(TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value), promptStyle);
                        console.WriteLine();
                        return DefaultValue.Value;
                    }

                    if (!AllowEmpty)
                    {
                        continue;
                    }
                }

                console.WriteLine();

                // Try convert the value to the expected type.
                if (!TextPrompt<T>.TryConvert(input, out var result) || result == null)
                {
                    console.MarkupLine(ValidationErrorMessage);
                    WritePrompt(console);
                    continue;
                }

                if (Choices.Count > 0)
                {
                    if (Choices.Contains(result))
                    {
                        return result;
                    }
                    else
                    {
                        console.MarkupLine(InvalidChoiceMessage);
                        WritePrompt(console);
                        continue;
                    }
                }

                // Run all validators
                if (!ValidateResult(result, out var validationMessage))
                {
                    console.MarkupLine(validationMessage);
                    WritePrompt(console);
                    continue;
                }

                return result;
            }
        }

        /// <summary>
        /// Writes the prompt to the console.
        /// </summary>
        /// <param name="console">The console to write the prompt to.</param>
        private void WritePrompt(IAnsiConsole console)
        {
            if (console is null)
            {
                throw new ArgumentNullException(nameof(console));
            }

            var builder = new StringBuilder();
            builder.Append(_prompt.TrimEnd());

            if (ShowChoices && Choices.Count > 0)
            {
                var choices = string.Join("/", Choices.Select(choice => TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(choice)));
                builder.AppendFormat(CultureInfo.InvariantCulture, " [blue][[{0}]][/]", choices);
            }

            if (ShowDefaultValue && DefaultValue != null)
            {
                builder.AppendFormat(
                    CultureInfo.InvariantCulture,
                    " [green]({0})[/]",
                    TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value));
            }

            var markup = builder.ToString().Trim();
            if (!markup.EndsWith("?", StringComparison.OrdinalIgnoreCase) &&
                !markup.EndsWith(":", StringComparison.OrdinalIgnoreCase))
            {
                markup += ":";
            }

            console.Markup(markup + " ");
        }

        /// <summary>
        /// Tries to convert the input string to <typeparamref name="T"/>.
        /// </summary>
        /// <param name="input">The input to convert.</param>
        /// <param name="result">The result.</param>
        /// <returns><c>true</c> if the conversion succeeded, otherwise <c>false</c>.</returns>
        [SuppressMessage("Design", "CA1031:Do not catch general exception types")]
        private static bool TryConvert(string input, [MaybeNull] out T result)
        {
            try
            {
                result = (T)TextPrompt<T>.GetTypeConverter().ConvertFromInvariantString(input);
                return true;
            }
            catch
            {
#pragma warning disable CS8601 // Possible null reference assignment.
                result = default;
#pragma warning restore CS8601 // Possible null reference assignment.
                return false;
            }
        }

        /// <summary>
        /// Gets the type converter that's used to convert values.
        /// </summary>
        /// <returns>The type converter that's used to convert values.</returns>
        private static TypeConverter GetTypeConverter()
        {
            var converter = TypeDescriptor.GetConverter(typeof(T));
            if (converter != null)
            {
                return converter;
            }

            var attribute = typeof(T).GetCustomAttribute<TypeConverterAttribute>();
            if (attribute != null)
            {
                var type = Type.GetType(attribute.ConverterTypeName, false, false);
                if (type != null)
                {
                    converter = Activator.CreateInstance(type) as TypeConverter;
                    if (converter != null)
                    {
                        return converter;
                    }
                }
            }

            throw new InvalidOperationException("Could not find type converter");
        }

        private bool ValidateResult(T value, [NotNullWhen(false)] out string? message)
        {
            if (Validator != null)
            {
                var result = Validator(value);
                if (!result.Successful)
                {
                    message = result.Message ?? ValidationErrorMessage;
                    return false;
                }
            }

            message = null;
            return true;
        }
    }
}
